--- Example code for making algebraic data work with JSON
module examples.JSONExample where

import Data.JSON

data Engine = Electric Double --- just performance in kW
            | Combustion { fuel :: [Fuel], displacement :: Double, cyls :: Int }

data Fuel = DIESEL | GASOLINE | NATURALGAS | LPG

data Vehicle = Vehicle { wheels :: Int, engine :: Maybe Engine }

derive Show Fuel
derive Show Engine
derive Show Vehicle

instance ToJSON Fuel where
    toJSON = String . show

instance ToJSON Engine where
    toJSON (Electric d) 
            = struct "Electric" d
    toJSON Combustion{fuel, displacement, cyls}
            = struct "Combustion" (fuel, displacement, cyls)

instance ToJSON Vehicle where
    {-- For illustration, we use a struct with record fields
        
        The first clause is not strictly necessary, 
        but helps to reduce the generated json size
        in the case that we have no engine, by just not
        producing an association for "engine".
        
        This assumes that the corresponding fromJSON takes care of that,
        preferably by extracting the "engine" field with 'optional'.
    -}
    toJSON Vehicle{wheels, engine=Nothing} 
            = Struct [ assoc "wheels" wheels ]   -- just leave out engine
    toJSON Vehicle{wheels, engine} 
            = Struct [
                    assoc "wheels" wheels,
                    assoc "engine" (maybeToJSON engine), 
            ]

bicycle = Vehicle { wheels = 2, engine = Nothing }
ebike   = Vehicle { wheels = 2, engine = Just (Electric 0.9) }
car     = Vehicle { wheels = 4, 
                    engine = Just Combustion {
                                  fuel = [LPG, GASOLINE], 
                                  displacement = 2.0,
                                  cyls = 4 }}
vehicles = [car, bicycle, ebike]

doubles = [1L .. 1_000_000L]

derive Eq Vehicle
derive Eq Engine
derive Eq Fuel

main = do
    let json = show (toJSON vehicles)
    println (toJSON vehicles)
    print "Parsing the above string back ... "
    let vs = parseJSON json :: Maybe [Vehicle]
    println (maybe "failed." (const "succeeded!") vs)
    print "And the result is "
    when (vs != Just vehicles) (print "NOT")
    println " the same as the original."
    println (parseJSON "{ \"Combustion\" : [ [\"Water\"], 2.7, 3]}" :: Either String Engine)
    println (parseJSON "{\"wheels\" : 3}" :: Maybe Vehicle)
    either println (return . const ()) (parseJSON "[\"not a vehicle?\"]" :: Either String Vehicle)
    either println (return . const ()) (parseJSON "{\"räder\" : 3}" :: Either String Vehicle)
    let jdbls = show (toJSON doubles)
    -- println jdbls
    println ("trying to read " ++ show (length doubles) ++ " longs")
    let xdbls = parseJSON jdbls :: Either String [Long]
    case xdbls of
        Left s = println s
        Right rdbls  = do   
            println ("succeeded and it " ++ (if rdbls == doubles then "IS " else "IS NOT ") ++ "the same")
            println (diff 0 rdbls doubles)
    -- parseJSON "[]" :: IO Vehicle  -- yes! IO is a MonadFail, guess what happens
    

diff !n [] [] = "no difference."
diff !n (a:as) (b:bs)
    | a != b = "difference at offset " ++ show n ++ ", a=" ++ show a ++ ", b=" ++ show b
    | otherwise = diff (n+1) as bs
diff !n (a:as) [] = "difference at offset " ++ show n ++ ", bs are null, but length as is " ++ show (1+length as)
diff !n [] (b:bs) = "difference at offset " ++ show n ++ ", as are null, but length bs is " ++ show (1+length bs)

instance FromJSON Fuel where
    fromJSON (String "DIESEL") = return DIESEL
    fromJSON (String "GASOLINE") = return GASOLINE
    fromJSON (String "NATURALGAS") = return NATURALGAS
    fromJSON (String "LPG") = return LPG
    fromJSON s = fail ("cannot decode fuel from " ++ show s)

import Data.List(lookup)  -- for looking up associations
instance FromJSON Engine where
   fromJSON (Struct as)
      | Just n   <- lookup "Electric" as   = Electric <$> fromJSON n
      | Just fdc <- lookup "Combustion" as = do
              (fuel, displacement, cyls) <- fromJSON fdc
              return Combustion{fuel, displacement, cyls}
   fromJSON x = fail ("invalid engine: " ++ show x)

instance FromJSON Vehicle where
   fromJSON (Struct as)  = do
        engine ← optional "engine" as
        wheels ← field    "wheels" as
        pure Vehicle{wheels, engine}
   fromJSON garbage = fail ("couldn't decode Vehicle from: " ++ show garbage)
